package com.msi;

import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;

import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.Logger;
import org.simpleframework.xml.ElementMap;
import org.simpleframework.xml.Root;

@Root
public class Graph {

    protected static final Logger log = Logger.getLogger(Graph.class);

    /**
     * all vertices in graph key: vertex ID value: vertex
     */
    @ElementMap(entry = "vertex", key = "vertexId", attribute = false, inline = true)
    Map<Integer, Vertex> vertices = new HashMap<Integer, Vertex>();

    /**
     * true if graph doesn't have coordinates
     */
    boolean hasCoordinates = false;

    /**
     * map vertexId to coordinates used in graph visualization
     */
    Map<Integer, Point2D> coordinateMap;

    private Graph() {
        boolean rootIsConfigured = Logger.getRootLogger().getAllAppenders()
                .hasMoreElements();
        if (!rootIsConfigured)
            BasicConfigurator.configure();
    }

    /**
     * @param matrix
     *            graph structure as matrix
     * @return
     */
    public static Graph createGraphByNeighbourMatrix(List<List<Integer>> matrix) {
        long start = System.currentTimeMillis();
        Graph graph = new Graph();
        int vertexCount = matrix.size();

        graph.initializeVertices(vertexCount);
        graph.createEdgesByNeighbourhoodMatrix(matrix, vertexCount);

        long stop = System.currentTimeMillis();
        log.info("Tworzenie grafu z macierzy zajelo:" + (stop - start));
        graph.printOnConsole();
        return graph;
    }

    /**
     * @param numberOfVertices
     *            how many vertices will be created
     * @param density
     *            Probability of creating single edge between two vertices
     */
    public Graph(int numberOfVertices, double density) {
        Random rand = new Random();
        /** generate vertices */
        for (int i = 0; i < numberOfVertices; ++i) {
            vertices.put(i, new Vertex(i));
        }

        /** create connections */
        for (int i = 0; i < numberOfVertices; ++i) {
            for (int j = i + 1; j < numberOfVertices; ++j) {
                if (rand.nextDouble() < density)
                    vertices.get(i).addNeighbour(vertices.get(j));
            }
        }
    }

    private void createEdgesByNeighbourhoodMatrix(List<List<Integer>> matrix,
            int vertexCount) {
        for (int i = 0; i < vertexCount; ++i)
            for (int j = i + 1; j < vertexCount; ++j)
                addEdgeIfExistsInMatrix(i, j, matrix);
    }

    private void addEdgeIfExistsInMatrix(int currentVertex, int neighbour,
            List<List<Integer>> matrix) {
        if (matrix.get(currentVertex).get(neighbour) == 1) {
            vertices.get(currentVertex).addNeighbour(vertices.get(neighbour));
        }
    }

    private void initializeVertices(int vertexCount) {
        for (int i = 0; i < vertexCount; ++i)
            vertices.put(i, new Vertex(i));
    }

    public void printOnConsole() {

        List<Vertex> verticesList = createSortedVericesList();
        log.info("rozpoczynamy rysowanie grafu w postaci list sasiedztwa");
        for (Vertex v : verticesList) {
            v.print();
            System.out.println("");
        }
    }

    private List<Vertex> createSortedVericesList() {
        List<Vertex> verticesList = getVerticesList();
        sortVerticesById(verticesList);
        return verticesList;
    }

    private List<Vertex> getVerticesList() {
        List<Vertex> verticesList = new ArrayList<Vertex>();

        Set<Entry<Integer, Vertex>> set = vertices.entrySet();
        for (Entry<Integer, Vertex> e : set) {
            verticesList.add(e.getValue());
        }
        return verticesList;
    }

    private void sortVerticesById(List<Vertex> verticesList) {
        Collections.sort(verticesList, new Comparator<Vertex>() {

            @Override
            public int compare(Vertex o1, Vertex o2) {
                int id1 = o1.getId();
                int id2 = o2.getId();
                if (id1 < id2)
                    return -1;
                if (id1 > id2)
                    return 1;
                return 0;
            }

        });
    }

    private void sortVerticesByDegree(List<com.msi.Vertex> verticesList) {
        Collections.sort(verticesList, new Comparator<Vertex>() {
            @Override
            public int compare(Vertex o1, Vertex o2) {
                int d1 = o1.getDegree();
                int d2 = o2.getDegree();
                if (d1 > d2)
                    return -1;
                if (d1 < d2)
                    return 1;
                return 0;
            }
        });
    }

    public Vertex getVertex(Integer id) {
        return vertices.get(id);
    }

    public int getSize() {
        return vertices.size();
    }

    public int getNumberOfColors() {
        return 0;
    }

    public Map<Integer, Vertex> getVertices() {
        return vertices;
    }

    public void setVertices(Map<Integer, Vertex> vertices) {
        this.vertices = vertices;
    }

    public void setCoordinates(Map<Integer, Point2D> map) {

        if (map == null)
            return;
        coordinateMap = map;
        /*
         * Set<Entry<Integer, Point2D>> set = map.entrySet(); for
         * (Entry<Integer, Point2D> e : set) { Vertex v =
         * vertices.get(e.getKey()); v.setX(e.getValue().getX());
         * v.setY(e.getValue().getY()); }
         */
        hasCoordinates = true;
    }

    public void resetColors() {
        for (Vertex v : vertices.values()) {
            v.resetColor();
        }
    }

    public int getNumberOfEdges() {
        int result = 0;
        for (Vertex v : vertices.values()) {
            result += v.getDegree();
        }
        return result / 2;
    }

    public int getNumberOfVertices() {
        return vertices.values().size();
    }

    public boolean hasCoordinates() {
        return hasCoordinates;
    }

    public Map<Integer, Point2D> getCoordinateMap() {
        return coordinateMap;
    }

    /**
     * Zwraca głęboką kopię grafu
     * 
     * @return głęboka kopia grafu
     */
    public Graph getDeepCopy() {
        // TODO poprawna obsługa współrzędnych (kopia coordinateMap)
        Graph g = new Graph();

        // dodanie samych wierzchołków bez krawędzi
        for (Vertex v : vertices.values()) {
            g.addVertex(v.createVertexCopyWithoutNeighbors());
        }

        // dodanie krawędzi
        for (Vertex v : vertices.values()) {
            for (Vertex neighbor : v.getNeighborhood()) {
                g.getVertex(v.getId()).addNeighbourOneWay(
                        g.getVertex(neighbor.getId()));
            }
        }
        return g;
    }

    public void addVertex(Vertex v) {
        if (!vertices.containsKey(v.getId())) {
            vertices.put(v.getId(), v);
        }
    }

    /**
     * Zwraca listę nieodwiedzonych jeszcze sąsiadów posortowaną po ich
     * stopniach
     * 
     * @param asc
     *            czy sortowanie ma być rosnące
     * @return lista posortowanych sąsiadów
     */
    public List<Vertex> getNotVisitedVerticesSortedByDegree(boolean asc) {
        List<Vertex> result = new ArrayList<Vertex>();
        for (Vertex v : vertices.values()) {
            if (!v.isVisited())
                result.add(v);
        }

        if (asc) {
            Collections.sort(result, new Comparator<Vertex>() {
                @Override
                public int compare(Vertex o1, Vertex o2) {
                    int d1 = o1.getDegree();
                    int d2 = o2.getDegree();
                    if (d1 < d2)
                        return -1;
                    if (d1 > d2)
                        return 1;
                    return 0;
                }
            });
        } else {
            sortVerticesByDegree(result);
        }

        return result;
    }

    public List<Vertex> getVerticesSortedByDegree() {
        List<Vertex> result = new ArrayList<Vertex>();
        for (Vertex v : vertices.values()) {
            result.add(v);
        }
        sortVerticesByDegree(result);
        return result;
    }
}
